// util methods
const _ = require('lodash')
const { join } = require('path')
const fs = require('fs')
const { inspect } = require('util')
const { isObject } = require('jsonql-params-validator')
const jsonqlErrors = require('jsonql-errors')
const {
  JsonqlResolverNotFoundError,
  getErrorByStatus,
  JsonqlError
} = jsonqlErrors;
const {
  BASE64_FORMAT,
  CONTENT_TYPE,
  QUERY_NAME,
  MUTATION_NAME,
  API_REQUEST_METHODS,
  PAYLOAD_PARAM_NAME,
  CONDITION_PARAM_NAME,
  RESOLVER_PARAM_NAME ,
  QUERY_ARG_NAME
} = require('jsonql-constants')
const { trim } = _;

// export a create debug method
const debug = require('debug')
/**
 * @param {string} name for id
 * @param {boolean} cond i.e. NODE_ENV==='development'
 * @return {void} nothing
 */
const getDebug = (name, cond = true) => (
  cond ? debug('jsonql-koa').extend(name) : () => {}
)

/**
 * using lodash to chain two functions
 * @param {function} mainFn function
 * @param {array} ...moreFns functions spread
 * @return {function} to accept the parameter for the first function
 */
const chainFns = (mainFn, ...moreFns) => (
  (...args) => {
    let chain = _( Reflect.apply(mainFn, null, args) ).chain()
    let ctn = moreFns.length;
    for (let i = 0; i < ctn; ++i) {
      chain = chain.thru(moreFns[i])
    }

    return chain.value()
  }
)

/**
 * DIY in Array
 * @param {array} arr to check from
 * @param {*} value to check against
 * @return {boolean} true on found
 */
const inArray = (arr, value) => !!arr.filter(a => a === value).length;

/**
 * From underscore.string library
 * @BUG there is a bug here with the non-standard name start with _
 * @param {string} str string
 * @return {string} dasherize string
 */
const dasherize = str => (
  trim(str)
    .replace(/([A-Z])/g, '-$1')
    .replace(/[-_\s]+/g, '-')
    .toLowerCase()
)

/**
 * Get document (string) byte length for use in header
 * @param {string} doc to calculate
 * @return {number} length
 */
const getDocLen = doc => Buffer.byteLength(doc, 'utf8')

/**
 * The koa ctx object is not returning what it said on the documentation
 * So I need to write a custom parser to check the request content-type
 * @param {object} req the ctx.request
 * @param {string} type (optional) to check against
 * @return {mixed} Array or Boolean
 */
const headerParser = (req, type) => {
  try {
    const headers = req.headers.accept.split(',')
    if (type) {
      return headers.filter(h => h === type)
    }
    return headers;
  } catch (e) {
    // When Chrome dev tool activate the headers become empty
    return [];
  }
}

/**
 * wrapper of above method to make it easier to use
 * @param {object} req ctx.request
 * @param {string} type of header
 * @return {boolean}
 */
const isHeaderPresent = (req, type) => {
  const headers = headerParser(req, type)
  return !!headers.length;
}

/**
 * @TODO need to be more flexible
 * @param {object} ctx koa
 * @param {object} opts configuration
 * @return {boolean} if it match
 */
const isJsonqlPath = (ctx, opts) => ctx.path === opts.jsonqlPath;

/**
 * combine two check in one and save time
 * @param {object} ctx koa
 * @param {object} opts config
 * @return {boolean} check result
 */
const isJsonqlRequest = (ctx, opts) => {
  const header = isHeaderPresent(ctx.request, opts.contentType)
  if (header) {
    return isJsonqlPath(ctx, opts)
  }
  return false;
}

/**
 * check if this is point to the jsonql console
 * @param {object} ctx koa context
 * @param {object} opts config
 * @return {boolean}
 */
const isJsonqlConsoleUrl = (ctx, opts) => (
  ctx.method === 'GET' && isJsonqlPath(ctx, opts)
)

/**
 * getting what is calling after the above check
 * @param {string} method of call
 * @return {mixed} false on failed
 */
const getCallMethod = method => {
  const [ POST, PUT ] = API_REQUEST_METHODS;
  switch (true) {
    case method === POST:
      return QUERY_NAME;
    case method === PUT:
      return MUTATION_NAME;
    default:
      return false;
  }
};

/**
 * @param {string} name
 * @param {string} type
 * @param {object} opts
 * @return {function}
 */
const getPathToFn = function(name, type, opts) {
  const dir = opts.resolverDir;
  const fileName = dasherize(name);
  let paths = [];
  if (opts.contract && opts.contract[type] && opts.contract[type].path) {
    paths.push(opts.contract[type].path);
  }
  paths.push( join(dir, type, fileName, 'index.js') )
  paths.push( join(dir, type, fileName + '.js') )
  // paths.push( join(dir, fileName + '.js') );
  const ctn = paths.length;
  for (let i=0; i<ctn; ++i) {
    if (fs.existsSync(paths[i])) {
      return paths[i];
    }
  }
  return false;
}

/**
 * wrapper method
 * @param {mixed} result of fn return
 * @return {string} stringify data
 */
const packResult = result => {
  return JSON.stringify({ data: result })
}

/**
 * Handle the output
 * @param {object} opts configuration
 * @return {function} with ctx and body as params
 */
const handleOutput = function(opts) {
  return function(ctx, body) {
    ctx.size = getDocLen(body)
    ctx.type = opts.contentType;
    ctx.status = 200;
    ctx.body = body;
  }
}

/**
 * handle HTML output for the web console
 * @param {object} ctx koa context
 * @param {string} body output content
 * @return {void}
 */
const handleHtmlOutput = function(ctx, body) {
  ctx.size = getDocLen(body)
  ctx.type = 'text/html';
  ctx.status = 200;
  ctx.body = body + ''; // just make sure its string output
}

/**
 * Port this from the CIS App
 * @param {string} key of object
 * @param {mixed} value of object
 * @return {string} of things we after
 */
const replaceErrors = function(key, value) {
  if (value instanceof Error) {
    var error = {};
    Object.getOwnPropertyNames(value).forEach(function (key) {
      error[key] = value[key];
    })
    return error;
  }
  return value;
}

/**
 * create readible string version of the error object
 * @param {object} error obj
 * @return {string} printable result
 */
const printError = function(error) {
  //return 'MASKED'; //error.toString();
  // return JSON.stringify(error, replaceErrors);
  return inspect(error, false, null, true)
}

/**
 * wrapper method - the output is trying to match up the structure of the Error sub class
 * @param {mixed} detail of fn error
 * @param {string} [className=JsonqlError] the errorName
 * @param {number} [statusCode=500] the original error code
 * @return {string} stringify error
 */
const packError = function(detail, className = 'JsonqlError', statusCode = 500, message = '') {
  return JSON.stringify({
    error: { detail, className, statusCode, message }
  })
}

/**
 * use the ctx to generate error output
 * V1.1.0 we render this as a normal output with status 200
 * then on the client side will check against the result object for error
 * @param {object} ctx context
 * @param {number} code 404 / 500 etc
 * @param {object} e actual error
 * @param {string} message if there is one
 * @param {string} name custom error class name
 */
const ctxErrorHandler = function(ctx, code, e, message = '') {
  const render = handleOutput({contentType: CONTENT_TYPE})
  let name;
  if (typeof code === 'string') {
    name = code;
    code = jsonqlErrors[name] ? jsonqlErrors[name].statusCode : -1;
  } else {
    name = jsonqlErrors.getErrorByStatus(code)
  }
  // preserve the message
  if (!message && e && e.message) {
    message = e.message;
  }
  return render(ctx, packError(e, name, code, message))
}

/**
 * Just a wrapper to be clearer what error is it
 * @param {object} ctx koa
 * @param {object} e error
 * @return {undefined} nothing
 */
const forbiddenHandler = (ctx, e) => (
  ctxErrorHandler(ctx, 403, e, 'JsonqlAuthorisationError')
)

/**
 * Like what the name said
 * @param {object} contract the contract json
 * @param {string} type query|mutation
 * @param {string} name of the function
 * @return {object} the params part of the contract
 */
const extractParamsFromContract = function(contract, type, name) {
  try {
    const result = contract[type][name];
    debug('extractParamsFromContract', result)
    if (!result) {
      debug(name, type, contract)
      throw new JsonqlResolverNotFoundError(name, type)
    }
    return result;
  } catch(e) {
    throw new JsonqlResolverNotFoundError(name, e)
  }
}

/**
 * Check several parameter that there is something in the param
 * @param {*} param input
 * @return {boolean}
 */
const isNotEmpty = function(param) {
  return param !== undefined && param !== false && param !== null && trim(param) !== '';
}

/**
 * Check if a json file is a contract or not
 * @param {*} contract input
 * @return {*} false on failed
 */
const isContractJson = (contract) => (
  isObject(contract) && (contract[QUERY_NAME] || contract[MUTATION_NAME])  ? contract : false
)

/**
 * Extract the args from the payload
 * @param {object} payload to work with
 * @param {string} type of call
 * @return {array} args
 */
const extractArgsFromPayload = function(payload, type) {
  switch (type) {
    case QUERY_NAME:
      return payload[QUERY_ARG_NAME];
    case MUTATION_NAME:
      return [
        payload[PAYLOAD_PARAM_NAME],
        payload[CONDITION_PARAM_NAME]
      ];
    default:
      throw new JsonqlError(`Unknown ${type} to extract argument from!`);
  }
}

// export
module.exports = {

  chainFns,

  inArray,
  getDebug,

  dasherize,
  headerParser,
  getPathToFn,
  getDocLen,
  packResult,
  packError,
  printError,
  ctxErrorHandler,
  forbiddenHandler,

  isJsonqlPath,
  isJsonqlRequest,
  isJsonqlConsoleUrl,

  getCallMethod,
  isHeaderPresent,
  extractParamsFromContract,

  isObject,
  isNotEmpty,
  isContractJson,

  handleOutput,
  handleHtmlOutput,
  extractArgsFromPayload
}
